﻿using System;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.Sdk.Query;
using Microsoft.Crm.SdkTypeProxy;

namespace CrmRulo.v4
{
    /// <summary>
    /// Uploads a set of rdl files to a Dynamics CRM deployment.
    /// </summary>
    public class Crm4ReportsUploader
    {
        /// <summary>
        /// Proxy to CRM v4.0 web service
        /// </summary>
        private readonly CrmService _crm;

        /// <summary>
        /// Class constructor
        /// </summary>
        /// <param name="crm">Connection to CRM Service</param>
        public Crm4ReportsUploader(CrmService crm)
        {
            _crm = crm;
        }

        /// <summary>
        /// Uploads all the *.rdl files found in directory to CRM
        /// v4.0 + v2011
        /// </summary>
        /// <param name="directoryPath">Path to directory</param>
        public void UploadAllReports(string directoryPath)
        {
            var configFilePath = directoryPath + @"\publishconfig.xml";
            if (!File.Exists(configFilePath))
            {
                CreateConfigFile(configFilePath);
                Console.WriteLine(@"Config file has been created. Please review before publishing.");
                return;
            }
            var config = XElement.Load(configFilePath);

            string publishDirectory = ".";

            var publishDirNode = config.Attribute("directory");
            if (publishDirNode != null)
                publishDirectory = publishDirNode.Value;

            foreach (var report in config.Elements("report"))
            {
                var att = report.Attribute("filename");
                Util.Assert(att != null, "Attribute 'filename' not found in report.");
                var reportFileName = String.Format("{0}\\{1}", publishDirectory, att.Value);
                if (!File.Exists(reportFileName))
                {
                    Console.WriteLine("ERROR: Report file '{0}' not found. Skipping.", reportFileName);
                    continue;
                }

                var visibilityCodes = report.Elements("visibility").Select(r => (int)Enum.Parse(typeof(ReportVisibilityCode), r.Value)).ToArray();
                var categoryCodes = report.Elements("category").Select(r => (int)Enum.Parse(typeof(ReportCategoryCode), r.Value)).ToArray();
                var entityLogicalNames = report.Elements("entity").Select(r => r.Value).ToArray();


                att = report.Attribute("name");
                Util.Assert(att != null, "Attribute 'name' not found in report.");
                var reportName = att.Value;

                string reportDescription = String.Empty;
                att = report.Attribute("description");
                if (att != null)
                    reportDescription = att.Value;

                var languageCode = -1; // All languages by default
                att = report.Attribute("languagecode");
                if (att != null)
                    languageCode = Int32.Parse(att.Value);

                string parentReportId = null;
                att = report.Attribute("parent");
                if (att != null)
                    parentReportId = FindParentReport(att.Value);

                var reportObject = new Report
                {
                    PathToReportFile = reportFileName,
                    Name = reportName,
                    EntityLogicalNames = entityLogicalNames,
                    VisibilityCodes = visibilityCodes,
                    CategoryCodes = categoryCodes,
                    ParentReportName = parentReportId,
                    LanguageCode = languageCode,
                    Description = reportDescription
                };

                UploadReport(reportObject);
            }
        }

        /// <summary>
        /// Searches for a report in CRM. Fails if more than one report with the given name exist.
        /// v4.0
        /// </summary>
        /// <param name="reportName">The name of the report.</param>
        /// <returns>The id of the report, null if not found.</returns>
        private report FindReportByName(string reportName)
        {
            var q = new QueryByAttribute
                        {
                            EntityName = "report",
                            ColumnSet = new ColumnSet(new[] { "reportid", "modifiedon" } ),
                            Attributes = new[] { "name" },
                            Values = new[] { reportName }
                        };
            var res = _crm.RetrieveMultiple(q);
            if (res.BusinessEntities.Count == 0)
                return null;
            if (res.BusinessEntities.Count > 1)
                throw new Exception("More than one report found with name: " + reportName);
            var report = (report)res.BusinessEntities[0];
            return report;
        }

        /// <summary>
        /// Searches for an existing report with the given name
        /// v2011 + v4.0
        /// </summary>
        /// <param name="reportName">The name of the report</param>
        /// <returns>The id of the report. Fails if not found or duplicate.</returns>
        private string FindParentReport(string reportName)
        {
            try
            {
                var parentReport = FindReportByName(reportName);
                if (parentReport == null)
                    throw new Exception("Parent report not found: " + reportName);
                return parentReport.reportid.Value.ToString();
            }
            catch (Exception ex)
            {
                throw new Exception("Error retrieving parent report.", ex);
            }
        }

        /// <summary>
        /// Creates a boilerplate configuration file for the reports found in the specified directory.
        /// v4.0 + v2011
        /// </summary>
        /// <param name="configFilePath">Path to resulting configuration file to be created</param>
        public void CreateConfigFile(string configFilePath)
        {
            var directoryPath = Path.GetDirectoryName(configFilePath);
            var config = new XElement("publish", new XAttribute("directory", directoryPath));
            var reportFiles = Directory.GetFiles(directoryPath).Where(f => f.EndsWith(".rdl"));

            foreach (var file in reportFiles)
            {
                var fileName = new FileInfo(file).Name;
                config.Add(new XElement("report",
                    new XAttribute("filename", fileName),
                    new XAttribute("name", fileName),
                    new XAttribute("description", fileName),
                    new XAttribute("parent", "PARENTREPORT"),
                    new XAttribute("languagecode", "-1"),

                        new XElement("entity", "ENTITYLOGICALNAME_1"),
                        new XElement("entity", "ENTITYLOGICALNAME_2"),

                        new XElement("visibility", ReportVisibilityCode.Form),
                        new XElement("visibility", ReportVisibilityCode.Grid),
                        new XElement("visibility", ReportVisibilityCode.ReportsGrid),

                        new XElement("category", ReportCategoryCode.SalesReports),
                        new XElement("category", ReportCategoryCode.ServiceReports),
                        new XElement("category", ReportCategoryCode.MarketingReports),
                        new XElement("category", ReportCategoryCode.AdministrativeReports)
                    ));
            }
            config.Save(configFilePath);
        }

        /// <summary>
        /// Uploads or updates a SQL Server Reporting Services report file to CRM
        /// v4.0
        /// </summary>
        private void UploadReport(Report r)
        {
            string reportText = File.ReadAllText(r.PathToReportFile);
            string reportFileName = Path.GetFileName(r.PathToReportFile);
            var modifiedFileTime = File.GetLastWriteTime(r.PathToReportFile);

            var report = new report
                             {
                                 name = r.Name,
                                 filename = reportFileName,
                                 bodytext = reportText,
                                 reporttypecode = new Picklist(1), // Reporting services
                                 languagecode = new CrmNumber(r.LanguageCode),
                                 ispersonal = new CrmBoolean(false),
                                 description = r.Description
                             };

            if (!String.IsNullOrEmpty(r.ParentReportName))
                report.parentreportid = new Lookup("report", new Guid(r.ParentReportName));

            Guid reportId;
            var existingReport = FindReportByName(r.Name);

            if (existingReport == null)
            {
                reportId = _crm.Create(report);
                Console.WriteLine(@"Creating report '{0}'", r.Name);
            }
            else
            {
                var modifiedReportTime = DateTime.Parse(existingReport.modifiedon.Value);
                if (modifiedReportTime < modifiedFileTime)
                {
                    reportId = existingReport.reportid.Value;
                    report.reportid = new Key(reportId);
                    RemoveCategoryInformation(reportId);
                    RemoveVisibilityInformation(reportId);
                    RemoveEntityCodeInformation(reportId);
                    _crm.Update(report);
                    Console.WriteLine(@"Updating report '{0}'", r.Name);
                }
                else
                {
                    Console.WriteLine(@"Report '{0}' is newer on server. Skipping update.", r.Name);
                    return;
                }
            }

            foreach (var visibility in r.VisibilityCodes)
                AddVisibility(reportId, visibility);
            foreach (var category in r.CategoryCodes)
                AddCategory(reportId, category);
            foreach (var entityLogicalName in r.EntityLogicalNames)
                AddEntityCode(reportId, entityLogicalName);
        }

        /// <summary>
        /// Creates an auxiliary reportentity record for a report
        /// v4.0
        /// </summary>
        /// <param name="reportId">The report id</param>
        /// <param name="entityLogicalName">The entity name to attach the report to</param>
        private void AddEntityCode(Guid reportId, string entityLogicalName)
        {
            var re = new reportentity
                         {
                             reportid = new Lookup("report", reportId),
                             objecttypecode = new EntityNameReference(entityLogicalName)
                         };
            _crm.Create(re);
        }

        /// <summary>
        /// Deletes all auxiliary reportentity records for a report
        /// v4.0
        /// </summary>
        /// <param name="reportId">The report id</param>
        private void RemoveEntityCodeInformation(Guid reportId)
        {
            var q = new QueryByAttribute
                        {
                            EntityName = "reportentity",
                            Attributes = new[] { "reportid" },
                            Values = new[] { reportId.ToString() }
                        };

            var res = _crm.RetrieveMultiple(q);
            foreach (reportentity re in res.BusinessEntities)
                _crm.Delete("reportentity", re.reportentityid.Value);
        }

        /// <summary>
        /// Creates an auxiliary reportcategory record for a report
        /// v4.0
        /// </summary>
        /// <param name="reportId">The report id</param>
        /// <param name="category">The category id  (see ReportCategory enum)</param>
        private void AddCategory(Guid reportId, int category)
        {
            var reportCategory = new reportcategory
                                     {
                                         reportid = new Lookup("report", reportId),
                                         categorycode = new Picklist(category)
                                     };
            _crm.Create(reportCategory);
        }

        /// <summary>
        /// Deletes all auxiliary reportcategory records for a report
        /// v4.0
        /// </summary>
        /// <param name="reportId">The report id</param>
        private void RemoveCategoryInformation(Guid reportId)
        {
            var q = new QueryByAttribute
                        {
                            EntityName = "reportcategory",
                            Attributes = new[] { "reportid" },
                            Values = new[] { reportId.ToString() }
                        };

            var res = _crm.RetrieveMultiple(q);
            foreach (reportcategory rc in res.BusinessEntities)
                _crm.Delete("reportcategory", rc.reportcategoryid.Value);
        }

        /// <summary>
        /// Creates an auxiliary reportvisibility record for a report
        /// v4.0
        /// </summary>
        /// <param name="reportId">The report id</param>
        /// <param name="visibility">The visility value identificator (see ReportVisibilityCode enum)</param>
        private void AddVisibility(Guid reportId, int visibility)
        {
            var rv = new reportvisibility
            {
                reportid = new Lookup("report", reportId),
                visibilitycode = new Picklist(visibility)
            };
            _crm.Create(rv);
        }

        /// <summary>
        /// Deletes all auxiliary reportvisibility records for a report
        /// v4.0
        /// </summary>
        /// <param name="reportId">The report id</param>
        private void RemoveVisibilityInformation(Guid reportId)
        {
            var q = new QueryByAttribute
            {
                EntityName = "reportvisibility",
                Attributes = new[] { "reportid" },
                Values = new[] { reportId.ToString()
            }
            };
            var res = _crm.RetrieveMultiple(q);
            foreach (reportvisibility rv in res.BusinessEntities)
                _crm.Delete("reportvisibility", rv.reportvisibilityid.Value);
        }
    }
}
